/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;

using Microsoft.Scripting.Ast;
using Microsoft.Scripting.Utils;
using System.Diagnostics;

namespace Microsoft.Scripting.Actions {

    /// <summary>
    /// Stitcher, a.k.a Rule inliner. Takes list of rules and produces LambdaExpression with the
    /// rules inlined, adding the "update" call at the very end.
    /// </summary>
    internal partial class Stitcher {
        /// <summary>
        /// LambdaExpression parameters
        /// </summary>
        private readonly ParameterExpression[] _lp;

        /// <summary>
        /// Mapping of the ParameterExpression (rule) -> ParameterExpression (lambda)
        /// </summary>
        private readonly Dictionary<ParameterExpression, ParameterExpression> _map = new Dictionary<ParameterExpression, ParameterExpression>();

        private Stitcher(ParameterExpression[] lp) {
            _lp = lp;
        }

        internal static LambdaExpression Stitch<T>(Rule<T>[] rules) where T : class {
            Type targetType = typeof(T);
            Type siteType = typeof(CallSite<T>);

            MethodInfo invoke = targetType.GetMethod("Invoke");
            ParameterInfo[] parameters = invoke.GetParameters();

            int length = rules.Length;

            ParameterExpression[] lp;
            Expression[] body = new Expression[length + 1];

            if (length == 1) {
                // only one rule, use its parameters and don't stitch..
                Rule<T> rule = rules[0];
                lp = AddCallSiteParameter(parameters, rule.Parameters);
                body[0] = rule.Binding;
            } else {
                // Many rules, stitch them together on a new set of parameters
                lp = MakeParameters(parameters);
                Stitcher stitch = new Stitcher(lp);

                for (int i = 0; i < rules.Length; i++) {
                    Rule<T> rule = rules[i];
                    body[i] = stitch.StitchRule(rule.Parameters, rule.Binding);
                }
            }

            body[rules.Length] = Expression.Return(
                Expression.Call(
                    Expression.Property(
                        Expression.Convert(lp[0], siteType),
                        siteType.GetProperty("Update")
                    ),
                    invoke,
                    lp
                )
            );

            return new LambdaExpression(
                Annotations.Empty,
                ExpressionType.Lambda,
                targetType,
                "_stub_",
                invoke.ReturnType,
                Expression.Block(body),
                new ReadOnlyCollection<ParameterExpression>(lp),
                false
            );
        }

        private Expression StitchRule(ReadOnlyCollection<ParameterExpression> parameters, Expression expression) {
            _map.Clear();
            for (int i = 0; i < parameters.Count; i++) {
                Debug.Assert(parameters[i].Type == _lp[i + 1].Type);
                _map[parameters[i]] = _lp[i + 1];
            }
            return StitchExpression(this, expression);
        }

        private static ParameterExpression[] AddCallSiteParameter(ParameterInfo[] pis, ReadOnlyCollection<ParameterExpression> expressions) {
            Debug.Assert(pis.Length > 0 && pis.Length - 1 == expressions.Count);

            ParameterExpression[] vars = new ParameterExpression[pis.Length];
            vars[0] = Expression.Parameter(pis[0].ParameterType, "$arg0");

            for (int i = 1; i < vars.Length; i++) {
                Debug.Assert(pis[i].ParameterType == expressions[i - 1].Type);
                vars[i] = expressions[i - 1];
            }

            return vars;
        }

        private static ParameterExpression[] MakeParameters(ParameterInfo[] pis) {
            ParameterExpression[] vars = new ParameterExpression[pis.Length];

            for (int i = 0; i < pis.Length; i++) {
                vars[i] = Expression.Parameter(pis[i].ParameterType, "$arg" + i);
            }

            return vars;
        }

        private static Expression StitchExpression(Stitcher s, Expression n) {
            if (n == null) return null;
            return _Stitchers[(int)n.NodeType](s, n);
        }


        private static bool StitchExpression(Stitcher s, ref Expression expression) {
            Expression o = expression;
            Expression n = StitchExpression(s, o);
            if ((object)n != (object)o) {
                expression = n;
                return true;
            } else {
                return false;
            }
        }

        private static bool StitchExpressions(Stitcher s, Expression[] expressions) {
            bool change = false;
            for (int i = 0; i < expressions.Length; i++) {
                Expression o, n;

                o = expressions[i];
                n = StitchExpression(s, o);

                if ((object)n != (object)o) {
                    expressions[i] = n;
                    change = true;
                }
            }
            return change;
        }

        #region Individual Expression Stitching

        // ActionExpression
        private static Expression StitchActionExpression(Stitcher s, Expression n) {
            ActionExpression e = (ActionExpression)n;
            Expression[] args = ArrayUtils.MakeArray(e.Arguments);
            if (StitchExpressions(s, args)) {
                return new ActionExpression(e.Annotations, e.BindingInfo, new ReadOnlyCollection<Expression>(args), e.Type);
            } else {
                return n;
            }
        }

        // AssignmentExpression
        private static Expression StitchAssignmentExpression(Stitcher s, Expression n) {
            AssignmentExpression e = (AssignmentExpression)n;
            Expression[] args = new Expression[] { e.Expression, e.Value };
            if (StitchExpressions(s, args)) {
                return new AssignmentExpression(e.Annotations, args[0], args[1], e.Type, e.BindingInfo);
            } else {
                return n;
            }
        }

        // BinaryExpression
        private static Expression StitchBinaryExpression(Stitcher s, Expression n) {
            BinaryExpression e = (BinaryExpression)n;
            //TODO: need to handle conversion lambda
            Expression[] args = new Expression[] { e.Left, e.Right };
            if (StitchExpressions(s, args)) {
                return new BinaryExpression(e.Annotations, e.NodeType, args[0], args[1], e.Type, e.Method);
            } else {
                return n;
            }
        }

        // Block
        private static Expression StitchBlock(Stitcher s, Expression n) {
            Block e = (Block)n;
            Expression[] args = ArrayUtils.MakeArray(e.Expressions);
            if (StitchExpressions(s, args)) {
                return new Block(e.Annotations, new ReadOnlyCollection<Expression>(args), e.Type);
            } else {
                return n;
            }
        }

        // BreakStatement
        private static Expression StitchBreakStatement(Stitcher s, Expression n) {
            return n;
        }

        // ConditionalExpression
        private static Expression StitchConditionalExpression(Stitcher s, Expression n) {
            ConditionalExpression e = (ConditionalExpression)n;
            Expression[] args = new Expression[] { e.Test, e.IfTrue, e.IfFalse };
            if (StitchExpressions(s, args)) {
                return new ConditionalExpression(e.Annotations, args[0], args[1], args[2], e.Type);
            } else {
                return n;
            }
        }

        // ConstantExpression
        private static Expression StitchConstantExpression(Stitcher s, Expression n) {
            return n;
        }

        // ContinueStatement
        private static Expression StitchContinueStatement(Stitcher s, Expression n) {
            return n;
        }

        // DeleteExpression
        private static Expression StitchDeleteExpression(Stitcher s, Expression n) {
            DeleteExpression e = (DeleteExpression)n;
            Expression arg = e.Expression;
            if (StitchExpression(s, ref arg)) {
                return new DeleteExpression(e.Annotations, arg, e.BindingInfo);
            } else {
                return n;
            }
        }

        // DoStatement
        private static Expression StitchDoStatement(Stitcher s, Expression n) {
            DoStatement e = (DoStatement)n;
            Expression[] args = new Expression[] { e.Test, e.Body };
            if (StitchExpressions(s, args)) {
                return new DoStatement(e.Annotations, e.Label, args[0], args[1]);
            } else {
                return n;
            }
        }

        // EmptyStatement
        private static Expression StitchEmptyStatement(Stitcher s, Expression n) {
            return n;
        }

        // ExtensionExpression
        private static Expression StitchExtensionExpression(Stitcher s, Expression n) {
            Expression e = Expression.ReduceToKnown(n);
            StitchExpression(s, ref e);
            return e;
        }

        // LocalScopeExpression
        private static Expression StitchLocalScopeExpression(Stitcher s, Expression n) {
            return n;
        }

        // InvocationExpression
        private static Expression StitchInvocationExpression(Stitcher s, Expression n) {
            InvocationExpression e = (InvocationExpression)n;
            Expression lambda = e.Expression;
            Expression[] args = ArrayUtils.MakeArray(e.Arguments);

            bool stitched = StitchExpression(s, ref lambda);
            if (StitchExpressions(s, args)) stitched = true;

            if (stitched) {
                return new InvocationExpression(e.Annotations, lambda, e.Type, e.BindingInfo as CallAction, new ReadOnlyCollection<Expression>(args));
            } else {
                return n;
            }
        }

        // LabeledStatement
        private static Expression StitchLabeledStatement(Stitcher s, Expression n) {
            LabeledStatement e = (LabeledStatement)n;
            Expression arg = e.Statement;
            if (StitchExpression(s, ref arg)) {
                return new LabeledStatement(e.Annotations, e.Label, arg);
            } else {
                return n;
            }
        }

        // LambdaExpression
        private static Expression StitchLambdaExpression(Stitcher s, Expression n) {
            LambdaExpression e = (LambdaExpression)n;
            Expression arg = e.Body;
            if (StitchExpression(s, ref arg)) {
                return new LambdaExpression(e.Annotations, e.NodeType, e.Type, e.Name, e.ReturnType, arg, e.Parameters, e.ParameterArray);
            } else {
                return n;
            }
        }

        // LoopStatement
        private static Expression StitchLoopStatement(Stitcher s, Expression n) {
            LoopStatement e = (LoopStatement)n;
            Expression[] args = new Expression[] { e.Test, e.Increment, e.Body, e.ElseStatement };
            if (StitchExpressions(s, args)) {
                return new LoopStatement(e.Annotations, e.Label, args[0], args[1], args[2], args[3]);
            } else {
                return n;
            }
        }

        // MemberExpression
        private static Expression StitchMemberExpression(Stitcher s, Expression n) {
            MemberExpression e = (MemberExpression)n;
            Expression arg = e.Expression;
            if (StitchExpression(s, ref arg)) {
                return new MemberExpression(e.Annotations, e.Member, arg, e.Type, e.BindingInfo as MemberAction);
            } else {
                return n;
            }
        }

        // MethodCallExpression
        private static Expression StitchMethodCallExpression(Stitcher s, Expression n) {
            MethodCallExpression e = (MethodCallExpression)n;
            Expression inst = e.Object;
            Expression[] args = ArrayUtils.MakeArray(e.Arguments);

            bool stitched = StitchExpression(s, ref inst);
            if (StitchExpressions(s, args)) stitched = true;

            if (stitched) {
                return new MethodCallExpression(e.Annotations, e.Type, e.BindingInfo as InvokeMemberAction, e.Method, inst, new ReadOnlyCollection<Expression>(args));
            } else {
                return n;
            }
        }

        // NewArrayExpression
        private static Expression StitchNewArrayExpression(Stitcher s, Expression n) {
            NewArrayExpression e = (NewArrayExpression)n;
            Expression[] args = ArrayUtils.MakeArray(e.Expressions);
            if (StitchExpressions(s, args)) {
                return new NewArrayExpression(e.Annotations, e.NodeType, e.Type, new ReadOnlyCollection<Expression>(args));
            } else {
                return n;
            }
        }

        // NewExpression
        private static Expression StitchNewExpression(Stitcher s, Expression n) {
            NewExpression e = (NewExpression)n;
            Expression[] args = ArrayUtils.MakeArray(e.Arguments);
            if (StitchExpressions(s, args)) {
                return new NewExpression(e.Annotations, e.Type, e.Constructor, new ReadOnlyCollection<Expression>(args), e.BindingInfo as CreateInstanceAction);
            } else {
                return n;
            }
        }

        // ParameterExpression
        private static Expression StitchParameterExpression(Stitcher s, Expression n) {
            ParameterExpression e = (ParameterExpression)n;
            return s._map[e];
        }

        // ReturnStatement
        private static Expression StitchReturnStatement(Stitcher s, Expression n) {
            ReturnStatement e = (ReturnStatement)n;
            Expression arg = e.Expression;
            if (StitchExpression(s, ref arg)) {
                return new ReturnStatement(e.Annotations, arg);
            } else {
                return n;
            }
        }

        // ScopeExpression
        private static Expression StitchScopeExpression(Stitcher s, Expression n) {
            ScopeExpression e = (ScopeExpression)n;
            Expression arg = e.Body;
            if (StitchExpression(s, ref arg)) {
                return new ScopeExpression(arg, e.Name, e.Annotations, e.Variables);
            } else {
                return n;
            }
        }

        // SwitchStatement
        private static Expression StitchSwitchStatement(Stitcher s, Expression n) {
            SwitchStatement e = (SwitchStatement)n;
            Expression test = e.TestValue;
            SwitchCase[] cases = ArrayUtils.MakeArray(e.Cases);

            bool changed = StitchExpression(s, ref test);

            for (int i = 0; i < cases.Length; i++) {
                SwitchCase sc = cases[i];
                Expression body = sc.Body;
                if (StitchExpression(s, ref body)) {
                    changed = true;
                    cases[i] = new SwitchCase(sc.IsDefault, sc.Value, body);
                }
            }
            if (changed) {
                return new SwitchStatement(e.Annotations, e.Label, test, new ReadOnlyCollection<SwitchCase>(cases));
            } else {
                return n;
            }
        }

        // ThrowStatement
        private static Expression StitchThrowStatement(Stitcher s, Expression n) {
            ThrowStatement e = (ThrowStatement)n;
            Expression arg = e.Value;
            if (StitchExpression(s, ref arg)) {
                return new ThrowStatement(e.Annotations, arg);
            } else {
                return n;
            }
        }

        // TryStatement
        private static Expression StitchTryStatement(Stitcher s, Expression n) {
            TryStatement e = (TryStatement)n;
            Expression[] args = new Expression[] { e.Body, e.FaultStatement, e.FinallyStatement };

            bool stitched = StitchExpressions(s, args);

            ReadOnlyCollection<CatchBlock> handlers = null;
            if (e.Handlers != null) {
                CatchBlock[] cblk = ArrayUtils.MakeArray(e.Handlers);

                for (int i = 0; i < cblk.Length; i++) {
                    CatchBlock cb = cblk[i];
                    Expression[] cbargs = new Expression[] { cb.Body, cb.Filter };
                    if (StitchExpressions(s, cbargs)) {
                        stitched = true;
                        cblk[i] = new CatchBlock(cb.Annotations, cb.Test, cb.Variable, cbargs[0], cbargs[1]);
                    }
                }
                handlers = new ReadOnlyCollection<CatchBlock>(cblk);
            }

            if (stitched) {
                return new TryStatement(e.Annotations, args[0], handlers, args[2], args[1]);
            } else {
                return n;
            }
        }

        // TypeBinaryExpression
        private static Expression StitchTypeBinaryExpression(Stitcher s, Expression n) {
            TypeBinaryExpression e = (TypeBinaryExpression)n;
            Expression arg = e.Expression;
            if (StitchExpression(s, ref arg)) {
                return new TypeBinaryExpression(e.Annotations, e.NodeType, arg, e.TypeOperand);
            } else {
                return n;
            }
        }

        // UnaryExpression
        private static Expression StitchUnaryExpression(Stitcher s, Expression n) {
            UnaryExpression e = (UnaryExpression)n;
            Expression arg = e.Operand;
            if (StitchExpression(s, ref arg)) {
                return new UnaryExpression(e.Annotations, e.NodeType, arg, e.Type, e.Method, e.BindingInfo);
            } else {
                return n;
            }
        }

        // VariableExpression
        private static Expression StitchVariableExpression(Stitcher s, Expression n) {
            return n;
        }

        // YieldStatement
        private static Expression StitchYieldStatement(Stitcher s, Expression n) {
            YieldStatement e = (YieldStatement)n;
            Expression arg = e.Expression;
            if (StitchExpression(s, ref arg)) {
                return new YieldStatement(e.Annotations, arg);
            } else {
                return n;
            }
        }

        #endregion
    }
}
